/*
 * Copyright (C) 2005 David Orme <djo@coconut-palm-software.com>
 * 
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     David Orme     - Initial API and implementation
 */
package org.eclipse.swt.nebula.widgets.compositetable;

import java.lang.reflect.Constructor;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.ListIterator;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.events.TraverseEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.nebula.widgets.compositetable.internal.DuckType;
import org.eclipse.swt.nebula.widgets.compositetable.internal.ISelectableRegionControl;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Layout;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.Slider;
import org.eclipse.swt.widgets.Widget;

/**
 * (non-API) Class InternalCompositeTable. This is the run-time
 * CompositeTableControl. It gets its prototype row and (optional) header
 * objects from its SWT parent, then uses them to implement an SWT virtual table
 * control.
 * 
 * @author djo
 */
class InternalCompositeTable extends Composite implements Listener {
	// The internal UI controls that make up this control.
	private Composite controlHolder = null;
	private Composite vSliderHolder = null;
	private Slider vSlider = null;
	private Composite hScroller;
	private Composite hSliderHolder = null;
	private Slider hSlider = null;
	EmptyTablePlaceholder emptyTablePlaceholder = null;

	// My parent CompositeTable
	private CompositeTable parent;

	// Property fields
	private int maxRowsVisible;
	private int numRowsInDisplay;
	private int numRowsInCollection;
	private int topRow;
	private int currentRow;
	private int currentColumn;

	// The visible/invisible row objects and bookeeping info about them
	private int currentVisibleTopRow = 0;
	private int numRowsVisible = 0;
	private LinkedList rows = new LinkedList();
	private LinkedList spareRows = new LinkedList();
	int clientAreaHeight;

	// The prototype header/row objects and Constructors so we can duplicate
	// them
	private Constructor headerConstructor;
	private Constructor rowConstructor;
	private Control headerControl;
	private Control myHeader = null;
	private Control rowControl;

	/**
	 * Constructor InternalCompositeTable. The usual SWT constructor. The same
	 * style bits are allowed here as are allowed on Composite.
	 * 
	 * @param parentControl
	 *            The SWT parent.
	 * @param style
	 *            Style bits.
	 */
	public InternalCompositeTable(Composite parentControl, int style) {
		super(parentControl, style);
		initialize();

		this.parent = (CompositeTable) parentControl;

		setBackground(parentControl.getBackground());
		controlHolder.addListener(SWT.MouseWheel, this);

		maxRowsVisible = parent.getMaxRowsVisible();
		numRowsInCollection = parent.getNumRowsInCollection();
		topRow = parent.getTopRow();

		headerConstructor = parent.getHeaderConstructor();
		rowConstructor = parent.getRowConstructor();
		headerControl = parent.getHeaderControl();
		rowControl = parent.getRowControl();
		
		setMenu(parent.getMenu());

		currentVisibleTopRow = topRow;
		showHeader();
		updateVisibleRows();

		if (numRowsVisible < 1) {
			createEmptyTablePlaceholer();
		}
	}

	public void setBackground(Color color) {
		super.setBackground(color);
		controlHolder.setBackground(color);
	}

	/**
	 * Initialize the overall table UI.
	 */
	private void initialize() {
		GridLayout gl = new GridLayout();
		gl.numColumns = 2;
		gl.verticalSpacing = 0;
		gl.marginWidth = 0;
		gl.marginHeight = 0;
		gl.horizontalSpacing = 0;
		this.setLayout(gl);
		createControlHolder();
		createVSliderHolder();
		createHSliderHolder();
	}

	/**
	 * Initialize the controlHolder, which is the holder Composite for the
	 * header object (if applicable) and the row objects.
	 */
	private void createControlHolder() {
		hScroller = new Composite(this, SWT.NULL);
		GridData gridData = new GridData();
		gridData.horizontalAlignment = GridData.FILL;
		gridData.grabExcessHorizontalSpace = true;
		gridData.grabExcessVerticalSpace = true;
		gridData.verticalAlignment = GridData.FILL;
		hScroller.setLayoutData(gridData);
		
		controlHolder = new Composite(hScroller, SWT.NONE);
		
		controlHolder.setLayout(new Layout() {
			protected Point computeSize(Composite composite, int wHint,
					int hHint, boolean flushCache) {
				if (rowControl != null) {
					int height = 0;
					int width = 0;
					if (headerControl != null) {
						Point headerSize = headerControl.computeSize(wHint, hHint, flushCache);
						width = headerSize.x;
						height = headerSize.y;
					}
					Point rowSize = rowControl.computeSize(wHint, hHint, flushCache);
					height += rowSize.y * 2;
					if (width < rowSize.x) {
						width = rowSize.x;
					}
					return new Point(width, height);
				}
				return new Point(50, 50);
			}

			protected void layout(Composite composite, boolean flushCache) {
				layoutControlHolder();
			}
		});
		
		hScroller.addControlListener(scrollerResizeHandler);
	}
	
	ControlAdapter scrollerResizeHandler = new ControlAdapter() {
		public void controlResized(ControlEvent e) {
            Point size = hScroller.getSize();

			int preferredWidth = controlHolder.computeSize(SWT.DEFAULT, 
                    SWT.DEFAULT, true).x;

			if (preferredWidth > size.x && !isHSliderVisible()) {
				setHSliderVisible(true);
			}
			if (preferredWidth <= size.x && isHSliderVisible()) {
				setHSliderVisible(false);
			}

			if (preferredWidth <= size.x) {
				controlHolder.setBounds(0, 0, size.x, size.y);
				return;
			}

			if (isHSliderVisible()) {
				hSlider.setMaximum(preferredWidth);
				hSlider.setPageIncrement(size.x);
				hSlider.setThumb(preferredWidth - (preferredWidth - size.x));
				int currentSelection = hSlider.getSelection();
				if (preferredWidth - currentSelection < size.x) {
					hSlider.setSelection(preferredWidth - size.x);
				}
			}
			hSlider.notifyListeners(SWT.Selection, new Event());
		}
	};
    
    
	/**
	 * Initialize the sliderHolder and slider. The SliderHolder is a Composite
	 * that is responsible for showing and hiding the vertical slider upon
	 * request.
	 */
	private void createVSliderHolder() {
		GridData gd = getVSliderGridData();
		vSliderHolder = new Composite(this, SWT.NULL);
		vSlider = new Slider(vSliderHolder, SWT.VERTICAL);
		vSlider.addSelectionListener(sliderSelectionListener);
		vSliderHolder.setLayout(new FillLayout());
		vSliderHolder.setLayoutData(gd);
		vSliderHolder.setTabList(new Control[] {});
	}

	/**
	 * Initialize the sliderHolder and slider. The SliderHolder is a Composite
	 * that is responsible for showing and hiding the vertical slider upon
	 * request.
	 */
	private void createHSliderHolder() {
		GridData gd = getHSliderGridData();
		hSliderHolder = new Composite(this, SWT.NULL);
		hSlider = new Slider(hSliderHolder, SWT.HORIZONTAL);
		hSlider.setMinimum(0);
		hSlider.setIncrement(20);
		hSlider.addSelectionListener(sliderSelectionListener);
		hSliderHolder.setLayout(new FillLayout());
		hSliderHolder.setLayoutData(gd);
		hSliderHolder.setTabList(new Control[] {});
		hSlider.addSelectionListener(hSliderSelectionListener);
	}

	// Slider utility methods
	// ---------------------------------------------------------------------

	/**
	 * Returns a GridData for the SliderHolder appropriate for if the slider is
	 * visible or not.
	 * 
	 * @return A GridData with a widthHint of 0 if the slider is not visible or
	 *         with a widthHint of SWT.DEFAULT otherwise.
	 */
	private GridData getVSliderGridData() {
		GridData gd = new GridData();
		gd.grabExcessVerticalSpace = true;
		gd.verticalAlignment = GridData.FILL;
		gd.verticalSpan = 1;
		if (!vSliderVisible) {
			gd.widthHint = 0;
		}
		return gd;
	}

	/**
	 * Returns a GridData for the SliderHolder appropriate for if the slider is
	 * visible or not.
	 * 
	 * @return A GridData with a heightHint of 0 if the slider is not visible or
	 *         with a heightHint of SWT.DEFAULT otherwise.
	 */
	private GridData getHSliderGridData() {
		GridData gd = new GridData();
		gd.grabExcessHorizontalSpace = true;
		gd.horizontalAlignment = GridData.FILL;
		gd.horizontalSpan = 1;
		if (!hSliderVisible) {
			gd.heightHint = 0;
		}
		return gd;
	}

	private boolean vSliderVisible = false;

	/**
	 * Sets if the slider is visible or not.
	 * 
	 * @param visible
	 *            true if the slider should be visible; false otherwise.
	 */
	public void setVSliderVisible(boolean visible) {
		this.vSliderVisible = visible;
		vSliderHolder.setLayoutData(getVSliderGridData());
		if (visible) {
			Display.getCurrent().addFilter(SWT.KeyDown, displayKeyDownFilter);
		} else {
			Display.getCurrent().removeFilter(SWT.KeyDown, displayKeyDownFilter);
		}
		Display.getCurrent().asyncExec(new Runnable() {
			public void run() {
				if (!InternalCompositeTable.this.isDisposed() &&
						!vSliderHolder.isDisposed() &&
						!vSliderHolder.getParent().isDisposed()) 
				{
					vSliderHolder.getParent().layout(true);
					vSliderHolder.layout(true);
					Point sliderHolderSize = vSliderHolder.getSize();
					vSlider.setBounds(0, 0, sliderHolderSize.x, sliderHolderSize.y);
				}
			}
		});
	}

	/**
	 * Returns if the slider is visible.
	 * 
	 * @return true if the slider is visible; false otherwise.
	 */
	public boolean isVSliderVisible() {
		return vSliderVisible;
	}

	private boolean hSliderVisible = false;

	/**
	 * Sets if the slider is visible or not.
	 * 
	 * @param visible
	 *            true if the slider should be visible; false otherwise.
	 */
	public void setHSliderVisible(boolean visible) {
		this.hSliderVisible = visible;
		hSliderHolder.setLayoutData(getHSliderGridData());
		Display.getCurrent().asyncExec(new Runnable() {
			public void run() {
				if (!InternalCompositeTable.this.isDisposed() &&
						!hSliderHolder.isDisposed() &&
						!hSliderHolder.getParent().isDisposed()) 
				{
					hSliderHolder.getParent().layout(true);
					hSliderHolder.layout(true);
					Point sliderHolderSize = hSliderHolder.getSize();
					hSlider.setBounds(0, 0, sliderHolderSize.x, sliderHolderSize.y);
				}
			}
		});
	}

	/**
	 * Returns if the slider is visible.
	 * 
	 * @return true if the slider is visible; false otherwise.
	 */
	public boolean isHSliderVisible() {
		return hSliderVisible;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.swt.widgets.Widget#dispose()
	 */
	public void dispose() {
		disposeRows(rows);
		disposeRows(spareRows);
		super.dispose();
	}

	/**
	 * Disposes all the row objects in the specified LinkedList.
	 * 
	 * @param rowsCollection
	 *            The collection containing TableRow objects to dispose.
	 */
	private void disposeRows(LinkedList rowsCollection) {
		for (Iterator rowsIter = rowsCollection.iterator(); rowsIter.hasNext();) {
			TableRow row = (TableRow) rowsIter.next();
			if (row instanceof IRowFocusListener) {
				parent.removeRowFocusListener((IRowFocusListener) row);
			}
			if (row instanceof IRowContentProvider) {
				parent.removeRowContentProvider((IRowContentProvider) row);
			}
			row.dispose();
		}
	}

	// Row object layout
	// --------------------------------------------------------------------------

	/**
	 * Layout the child controls within the controlHolder Composite.
	 */
	protected void layoutControlHolder() {
		if (myHeader != null) {
            layoutHeaderOrRow(myHeader);
        }
		for (Iterator rowsIter = rows.iterator(); rowsIter.hasNext();) {
			TableRow row = (TableRow) rowsIter.next();
            layoutHeaderOrRow(row.getRowControl());
		}
		updateVisibleRows();
	}
    
    private void layoutHeaderOrRow(Control control) {
        if (control instanceof Composite) {
            Composite headerOrRow = (Composite) control;
            headerOrRow.layout(true);
        }
    }

	// Table control layout -- utility methods
	// ----------------------------------------------------

	/**
	 * Construct a header or row object on demand. Logs an error and returns
	 * null on failure.
	 * 
	 * @param parent
	 *            The SWT parent.
	 * @param constructor
	 *            The header or row object's constructor.
	 * @return The constructed control or null if none could be constructed.
	 */
	private Control createInternalControl(Composite parent,
			Constructor constructor) {
		Control result = null;
		try {
			if (!constructor.isAccessible()) {
				constructor.setAccessible(true);
			}
			result = (Control) constructor.newInstance(new Object[] { parent,
					new Integer(SWT.NULL) });
		} catch (Exception e) {
			throw new IllegalArgumentException("Unable to construct control"); //$NON-NLS-1$
		}
		if (result instanceof IRowFocusListener) {
			this.parent.addRowFocusListener((IRowFocusListener) result);
		}
		if (result instanceof IRowContentProvider) {
			this.parent.addRowContentProvider((IRowContentProvider) result);
		}
		return result;
	}

	/**
	 * If the header control hasn't been created yet, create and show it.
	 */
	private void showHeader() {
		if (myHeader == null && headerConstructor != null) {
			myHeader = createInternalControl(controlHolder, headerConstructor);
			fireHeaderConstructionEvent(myHeader);
			if (myHeader instanceof Composite) {
				Composite headerComp = (Composite) myHeader;
				if (headerComp.getLayout() instanceof GridRowLayout) {
					headerComp.addPaintListener(headerPaintListener);
				}
			}
            layoutHeaderOrRow(myHeader);
		}
	}

	// Table control layout -- main refresh algorithm
	// ---------------------------------------------

	/**
	 * Main refresh algorithm entry point. This method refreshes everything in
	 * the table:
	 * 
	 * <ul>
	 * <li>Makes sure the correct number of rows are visible
	 * <li>Makes sure each row has been refreshed with data from the underlying
	 * model
	 * </ul>
	 */
	void updateVisibleRows() {
		// If we don't have our prototype row object yet, bail out
		if (rowControl == null) {
			return;
		}
		
		clientAreaHeight = controlHolder.getSize().y;
		if (clientAreaHeight <= 0) {
			return;
		}

		int topPosition = 0;

		int headerHeight = 0;
		if (myHeader != null) {
			headerHeight = headerControl.getSize().y + 3;
			clientAreaHeight -= headerHeight;
			topPosition += headerHeight;
		}
		numRowsInDisplay = clientAreaHeight / getRowHeight(clientAreaHeight);

		// Make sure we have something to lay out to begin with
		int userScrollDirection = 0;
		if (numRowsInCollection > 0) {
			numRowsVisible = numRowsInDisplay;

			disposeEmptyTablePlaceholder();

			int displayableRows = numRowsInCollection - topRow;
			if (numRowsVisible > displayableRows) {
				numRowsVisible = displayableRows;
			}
			if (numRowsVisible > maxRowsVisible) {
				numRowsVisible = maxRowsVisible;
			}
			if (numRowsVisible < 1) {
				numRowsVisible = 1;
			}

			// Keep track of if we're scrolling forwards or backwards
			if (currentVisibleTopRow - topRow > 0) {
				userScrollDirection = ScrollEvent.BACKWARD;
			} else if (topRow - currentVisibleTopRow > 0) {
				userScrollDirection = ScrollEvent.FORWARD;
			}

			// Scroll the view so that the right number of row
			// objects are showing and they have the right data
			if (rows.size() - Math.abs(currentVisibleTopRow - topRow) > 0) {
//				if (currentRow >= numRowsVisible) {
//					deleteRowAt(0);
//					++currentVisibleTopRow;
//					++topRow;
//					--currentRow;
//				}
				scrollTop();
				fixNumberOfRows();
			} else {
				currentVisibleTopRow = topRow;
				fixNumberOfRows();
				refreshAllRows();
			}
		} else {
			numRowsVisible = 0;
			topRow = 0;
			currentRow = 0;
			currentColumn = 0;
			currentVisibleTopRow = 0;
			numRowsVisible = 0;

			if (emptyTablePlaceholder == null) {
				fixNumberOfRows();
				createEmptyTablePlaceholer();
			}
		}
		
		// Make sure that the currentRow is within the visible range
		// (after PgDn, it could wind up outside the visible range)
		if (currentRow >= numRowsVisible && getNumRowsVisible() < numRowsInDisplay) {
			currentRow = numRowsVisible-1;
		}

		// Show, hide, reset the scroll bar
		if (numRowsVisible < numRowsInCollection) {
			int extra = numRowsInCollection - numRowsVisible;
			int pageIncrement = numRowsVisible;
			if (pageIncrement > extra)
				pageIncrement = extra;

			vSlider.setMaximum(numRowsInCollection);
			vSlider.setMinimum(0);
			vSlider.setIncrement(1);
			vSlider.setPageIncrement(pageIncrement);
			vSlider.setThumb(numRowsInCollection
					- (numRowsInCollection - numRowsVisible));

			vSlider.setSelection(topRow);

			if (!isVSliderVisible()) {
				setVSliderVisible(true);
			}
		} else {
			setVSliderVisible(false);
		}

		// Lay out the header and rows correctly in the display
		int width = controlHolder.getSize().x;

		// First, the header...
		if (myHeader != null) {
			myHeader.setBounds(0, 0, width, headerHeight);
		}

		// Make sure we have rows to lay out...
		if (numRowsInCollection < 1) {
			return;
		}

		// Now the rows.
		int rowHeight = getRowHeight(clientAreaHeight);

		// We have to move the controls front-to-back if we're scrolling
		// forwards and back-to-front if we're scrolling backwards to avoid ugly
		// screen refresh artifacts.
		if (userScrollDirection == ScrollEvent.FORWARD || userScrollDirection == ScrollEvent.NONE) {
			for (Iterator rowsIter = rows.iterator(); rowsIter.hasNext();) {
				TableRow row = (TableRow) rowsIter.next();
				Control rowControl = row.getRowControl();
				rowControl.setBounds(0, topPosition, width, rowHeight);
                layoutHeaderOrRow(rowControl);
				topPosition += rowHeight;
			}
		} else {
			ListIterator rowsIter = rows.listIterator();
			while (rowsIter.hasNext()) {
				rowsIter.next();
			}
			topPosition += rowHeight * (rows.size() - 1);
			while (rowsIter.hasPrevious()) {
				TableRow row = (TableRow) rowsIter.previous();
				Control rowControl = row.getRowControl();
				rowControl.setBounds(0, topPosition, width, rowHeight);
                layoutHeaderOrRow(rowControl);
				topPosition -= rowHeight;
			}
		}
		
		// If we scrolled, tell clients about it
		if (userScrollDirection != ScrollEvent.NONE) {
			fireScrollEvent(new ScrollEvent(userScrollDirection, parent));
		}
	}

	int getRowHeight(int clientAreaHeight) {
		int rowControlHeight = rowControl.getSize().y;
		if (maxRowsVisible == Integer.MAX_VALUE) {
			return rowControlHeight;
		}
		return rowControlHeight;
	}

	/**
	 * Utility method: Makes sure that the currently visible top row is the same
	 * as the top row specified in the TopRow property.
	 */
	private void scrollTop() {
		while (currentVisibleTopRow < topRow) {
			deleteRowAt(0);
			++currentVisibleTopRow;
		}
		while (currentVisibleTopRow > topRow) {
			--currentVisibleTopRow;
			insertRowAt(0);
		}
	}

	/**
	 * Utility method: Makes sure that the number of rows that are visible
	 * correspond with what should be visible given the table control's size,
	 * where it is scrolled, and the number of rows in the underlying
	 * collection.
	 */
	private void fixNumberOfRows() {
		int numRows = rows.size();
		while (numRows > numRowsVisible) {
			deleteRowAt(numRows - 1);
			numRows = rows.size();
		}
		while (numRows < numRowsVisible) {
			insertRowAt(numRows);
			numRows = rows.size();
		}
	}

	/**
	 * Fire the refresh event on all visible rows.
	 */
	void refreshAllRows() {
		int row = 0;
		for (Iterator rowsIter = rows.iterator(); rowsIter.hasNext();) {
			TableRow rowControl = (TableRow) rowsIter.next();
			fireRefreshEvent(topRow + row, rowControl.getRowControl());
			++row;
		}
      resetFocus();
	}

	/**
	 * Make sure that something sane inside the table has focus.
	 */
	private void resetFocus() {
		/*
		 * FEATURE IN WINDOWS: When we refresh all rows and one already has
		 * focus, Windows gets into a schizophrenic state where some part of
		 * Windows thinks that the current control has focus and another part of
		 * Windows thinks that the current control doesn't have focus.
		 * 
		 * The symptom is that the current control stops receiving events from
		 * Windows but Windows still thinks the current control has focus and so
		 * it won't give the control complete focus if you click on it.
		 * 
		 * The workaround is to set the focus away from the currently-focused
		 * control and to set it back.
		 */
		if (numRowsVisible < 1 || currentRow < 0) {
			return;
		}
		Control control = null;
		if (currentRow < numRowsVisible) {
			control = getControl(currentColumn, currentRow);
		} else if (currentRow > 0) {
			control = getControl(currentColumn, numRowsVisible - 1);
		}
		if (control != null && control.isFocusControl()) {
			this.setFocus();
			deferredSetFocus(control, true);
		}
	}

	/**
	 * Insert a new row object at the specified 0-based position relatve to the
	 * topmost row.
	 * 
	 * @param position
	 *            The 0-based position relative to the topmost row.
	 */
	private void insertRowAt(int position) {
		TableRow newRow = getNewRow();
		if (position > rows.size()) {
			position = rows.size();
		}
		rows.add(position, newRow);
		fireRefreshEvent(currentVisibleTopRow + position, newRow
				.getRowControl());
	}

	/**
	 * Delete the row at the specified 0-based position relative to the topmost
	 * row.
	 * 
	 * @param position
	 *            The 0-based position relative to the topmost row.
	 */
	private void deleteRowAt(int position) {
		TableRow row = (TableRow) rows.remove(position);
		row.setVisible(false);
		spareRows.addLast(row);
	}

	/**
	 * Utility method: Creates a new row object or recycles one that had been
	 * previously created but was no longer needed.
	 * 
	 * @return The new row object.
	 */
	private TableRow getNewRow() {
		if (spareRows.size() > 0) {
			TableRow recycledRow = (TableRow) spareRows.removeFirst();
			recycledRow.setVisible(true);
			return recycledRow;
		}
		Control newControl = createInternalControl(controlHolder,
				rowConstructor);
		if (menu != null) {
			newControl.setMenu(menu);
		}
		fireRowConstructionEvent(newControl);
		TableRow newRow = new TableRow(this, newControl);
		if (newRow.getRowControl() instanceof Composite) {
			Composite rowComp = (Composite) newRow.getRowControl();
			if (rowComp.getLayout() instanceof GridRowLayout) {
				rowComp.setBackground(getBackground());
				rowComp.addPaintListener(rowPaintListener);
			}
		}
		return newRow;
	}

	// Property getters/setters
	// --------------------------------------------------------------

	/*
	 * These are internal API. <p> Plese refer to the JavaDoc on CompositeTable
	 * for detailed description of these property methods.
	 */

	/**
	 * (non-API) Method getHeaderControl. Return the prototype control being
	 * used as a header.
	 * 
	 * @return The header control
	 */
	public Control getHeaderControl() {
		return headerControl;
	}

	/**
	 * Method setMaxRowsVisible. Sets the maximum number of rows that will be
	 * permitted in the table at once. For example, setting this property to 1
	 * will have the effect of creating a single editing area with a scroll bar
	 * on the right allowing the user to scroll through all rows using either
	 * the mouse or the PgUp/PgDn keys. The default value is Integer.MAX_VALUE.
	 * 
	 * @param maxRowsVisible
	 *            the maximum number of rows that are permitted to be visible at
	 *            one time, regardless of the control's size.
	 */
	public void setMaxRowsVisible(int maxRowsVisible) {
		this.maxRowsVisible = maxRowsVisible;
		updateVisibleRows();
	}

	/**
	 * Method getNumRowsVisible. Returns the actual number of rows that are
	 * currently visible. Normally CompositeTable displays as many rows as will
	 * fit vertically given the control's size. This value can be clamped to a
	 * maximum using the MaxRowsVisible property.
	 * 
	 * @return the actual number of rows that are currently visible.
	 */
	public int getNumRowsVisible() {
		return rows.size();
	}

	/**
	 * Method setNumRowsInCollection. Sets the number of rows in the data
	 * structure that is being edited.
	 * 
	 * @param numRowsInCollection
	 *            the number of rows represented by the underlying data
	 *            structure.
	 */
	public void setNumRowsInCollection(int numRowsInCollection) {
		this.topRow = 0;
		if (topRow + currentRow > numRowsInCollection) {
			if (topRow < numRowsInCollection) {
				currentRow = numRowsInCollection - topRow;
			} else {
				topRow = numRowsInCollection - 1;
				currentRow = 0;
			}
			deferredSetFocus(getCurrentRowControl(), false);
		}
		this.numRowsInCollection = numRowsInCollection;
		updateVisibleRows();
		refreshAllRows();
	}

	/**
	 * Method setTopRow. Set the number of the line that is being displayed in
	 * the top row of the CompositeTable editor (0-based). If the new top row is
	 * not equal to the current top row, the table will automatically be
	 * scrolled to the new position. This number must be greater than 0 and less
	 * than NumRowsInCollection.
	 * 
	 * @param topRow
	 *            the line number of the new top row.
	 */
	public void setTopRow(int topRow) {
		fireRowDepartEvent();
		this.topRow = topRow;
		updateVisibleRows();
		fireRowArriveEvent();
	}

	/**
	 * Method getTopRow. Return the number of the line that is being displayed
	 * in the top row of the CompositeTable editor (0-based).
	 * 
	 * @return the number of the top line.
	 */
	public int getTopRow() {
		return topRow;
	}

	/**
	 * Method getSelection. Returns the currently-selected (column, row) pair
	 * where the row specifies the offset from the top of the table window. In
	 * order to get the current row in the underlying data structure, use
	 * getSelection().y + getCurrentRow().
	 * 
	 * @return the currently-selected (column, row) pair where the row specifies
	 *         the offset from the top of the table window.
	 */
	public Point getSelection() {
		return new Point(currentColumn, currentRow);
	}

	/**
	 * Method setSelection. Sets the currently-selected (column, row) pair where
	 * the row specifies the offset from the top of the table window. In order
	 * to get the current row in the underlying data structure, use
	 * getSelection().y + getCurrentRow().
	 * 
	 * @param column
	 *            the column to select
	 * @param row
	 *            the row to select
	 */
	public void setSelection(int column, int row) {
		int topRowDelta = computeTopRowDelta(row);
		if (topRowDelta != 0) {
			setTopRow(topRow + topRowDelta);
			row += -1 * topRowDelta;
			internalSetSelection(column, row, true);
		} else {
			if (row == currentRow)
				internalSetSelection(column, row, false);
			else {
				if (fireRequestRowChangeEvent())
					internalSetSelection(column, row, true);
			}
		}
	}

	/**
	 * Method setWeights. Indicates that the column weights were just set and we
	 * should re-layout the control holder object.
	 */
	public void setWeights() {
		layoutControlHolder();
	}

	// Refresh Event API
	// --------------------------------------------------------------------------

	/**
	 * Adds the specified listener to the set of listeners that will be notified
	 * when a row refresh event occurs.
	 * 
	 * @param listener
	 *            the listener to add.
	 */
	public void addRefreshContentProvider(IRowContentProvider listener) {
		parent.contentProviders.add(listener);
	}

	/**
	 * Remove the specified listener from the set of listeners that will be
	 * notified when a row refresh event occurs.
	 * 
	 * @param listener
	 *            the listener to remove.
	 */
	public void removeRefreshContentProvider(IRowContentProvider listener) {
		parent.contentProviders.remove(listener);
	}

	private void fireRefreshEvent(int positionInCollection, Control rowControl) {
		if (numRowsInCollection < 1) {
			return;
		}
		for (Iterator refreshListenersIter = parent.contentProviders.iterator(); refreshListenersIter
				.hasNext();) {
			IRowContentProvider listener = (IRowContentProvider) refreshListenersIter
					.next();
			listener.refresh(parent, positionInCollection, rowControl);
		}
	}

	// Empty table placeholder
	// --------------------------------------------------------------------

	private void createEmptyTablePlaceholer() {
		emptyTablePlaceholder = new EmptyTablePlaceholder(controlHolder,
				SWT.NULL);
		if (rowControl != null)
			emptyTablePlaceholder.setBackground(rowControl.getBackground());
		emptyTablePlaceholder.setMessage(parent.getInsertHint());
	}

	private void disposeEmptyTablePlaceholder() {
		if (emptyTablePlaceholder != null) {
			emptyTablePlaceholder.dispose();
			emptyTablePlaceholder = null;
		}
	}

	// Event Handling
	// -----------------------------------------------------------------------------

	private boolean needToRequestRC = true;

	private Listener displayKeyDownFilter = new Listener() {
		public void handleEvent(Event event) {
			if (rowControl.getClass().isAssignableFrom(event.widget.getClass())) {
				doMakeFocusedRowVisible();
			}
		}
	};
    

	
	/**
	 * Handle a keyPressed event on any row control.
	 * 
	 * @param sender
	 *            The row that is sending the event
	 * @param e
	 *            the actual KeyEvent
	 */
	public void keyPressed(TableRow sender, KeyEvent e) {
		if (doMakeFocusedRowVisible()) return;
		
		if ((e.stateMask & SWT.CONTROL) != 0) {
			switch (e.keyCode) {
			case SWT.HOME:
                doFocusInitialRow();
				return;
			case SWT.END:
                doFocusLastRow();
				return;
			case SWT.INSERT:
                doInsertRow();
				return;
			case SWT.DEL:
                doDeleteRow();
				return;
			default:
				return;
			}
		}
		switch (e.keyCode) {
		case SWT.ARROW_UP:
            doRowUp();
			return;
		case SWT.ARROW_DOWN:
            doRowDown();
			return;
		case SWT.PAGE_UP:
            doPageUp();
			return;
		case SWT.PAGE_DOWN:
            doPageDown();
			return;
		}
	}
	
	/**
	 * Handle the keyTraversed event on any child control in the table.
	 * 
	 * @param sender
	 *            The row sending the event.
	 * @param e
	 *            The SWT TraverseEvent
	 */
	public void keyTraversed(TableRow sender, TraverseEvent e) {
		if (doMakeFocusedRowVisible()) return;

		if (parent.isTraverseOnTabsEnabled()) {
			if (e.detail == SWT.TRAVERSE_TAB_NEXT) {
				if (currentColumn >= sender.getNumColumns() - 1) {
					e.detail = SWT.TRAVERSE_NONE;
					handleNextRowNavigation();
				}
			} else if (e.detail == SWT.TRAVERSE_TAB_PREVIOUS) {
				if (currentColumn == 0) {
					e.detail = SWT.TRAVERSE_NONE;
					handlePreviousRowNavigation(sender);
				}
			} else if (e.detail == SWT.TRAVERSE_RETURN) {
				e.detail = SWT.TRAVERSE_NONE;
				if (currentColumn >= sender.getNumColumns() - 1) {
					handleNextRowNavigation();
				} else {
					deferredSetFocus(getControl(currentColumn + 1, currentRow),
							false);
				}
			}
		} else {
			if (e.detail == SWT.TRAVERSE_TAB_NEXT) {
				if (currentColumn >= sender.getNumColumns() - 1) {
					e.detail = SWT.TRAVERSE_NONE;
				}
			} else if (e.detail == SWT.TRAVERSE_TAB_PREVIOUS) {
				if (currentColumn == 0) {
					e.detail = SWT.TRAVERSE_NONE;
				}
			}
		}
	}

	/**
	 * Makes sure that the focused row is visible
	 * 
	 * @return true if the display needed to be scrolled; false otherwise
	 */
	public boolean doMakeFocusedRowVisible() {
		if (numRowsVisible < 1) {
			return false;
		}
		int topRowDelta = computeTopRowDelta(currentRow);
		if (topRowDelta == 0) {
			return false;
		}
		currentRow += -1 * topRowDelta;
		
		setTopRow(topRow + topRowDelta);
		Control control = getControl(currentColumn, currentRow);
		if (control != null) {
			control.setFocus(); // ?? Can I get away with avoiding asyncExec here ??
		}
		return true;
	}
	
	private int computeTopRowDelta(int row) {
		int topRowDelta;
		if (row < 0) {
			topRowDelta = row;
		} else if (row >= getNumRowsVisible()) {
			topRowDelta = row - getNumRowsVisible() + 1;
		} else {
			return 0;
		}
		return topRowDelta;
	}
	
	/**
	 * The SelectionListener for the table's vertical slider control.
	 */
	private SelectionListener sliderSelectionListener = new SelectionListener() {
		public void widgetSelected(SelectionEvent e) {
			if (vSlider.getSelection() == topRow) {
				return;
			}

			if (!fireRequestRowChangeEvent()) {
				vSlider.setSelection(topRow);
				return;
			}

			deselectCurrentRowIfVisible();

			int delta = topRow - vSlider.getSelection();
			int oldCurrentRow = currentRow;
			currentRow += delta;
			
			setTopRow(vSlider.getSelection());
			
			// If the focused row just became visible, show the focus
			if (oldCurrentRow < 0 || oldCurrentRow >= getNumRowsVisible()) {
				if (currentRow >= 0 && currentRow < getNumRowsVisible()) {
					Control newFocusControl = getControl(currentColumn, currentRow);
					if (newFocusControl == null) {
						return;
					}
					if (newFocusControl.isFocusControl()) {
						newFocusControl.notifyListeners(SWT.FocusIn, new Event());
					} else {
						deferredSetFocus(newFocusControl, true);
					}
				}
			} else {
				// If the new viewport doesn't overlap the old one, hide the focus
				if (currentRow < 0 || currentRow >= getNumRowsVisible()) {
					//					deleteRowAt(oldCurrentRow);
//					getControl(currentColumn, oldCurrentRow).getParent().setVisible(false);
//					getControl(currentColumn, oldCurrentRow).getParent().setVisible(true);
					Control control = getControl(currentColumn, oldCurrentRow);
					if (control != null) {
						control.notifyListeners(SWT.FocusOut, new Event());
					}
				}
			}
		}

		public void widgetDefaultSelected(SelectionEvent e) {
			widgetSelected(e);
		}
	};
	
	private SelectionListener hSliderSelectionListener = new SelectionListener() {
		/* (non-Javadoc)
		 * @see org.eclipse.swt.events.SelectionListener#widgetSelected(org.eclipse.swt.events.SelectionEvent)
		 */
		public void widgetSelected(SelectionEvent e) {
			Point scrollerSize = hScroller.getSize();
            int preferredWidth = controlHolder.computeSize(SWT.DEFAULT, 
                    SWT.DEFAULT, true).x;
			Rectangle controlHolderBounds = 
                new Rectangle(-1 * hSlider.getSelection(), 0, preferredWidth, 
                        scrollerSize.y);
			controlHolder.setBounds(controlHolderBounds);
		}
		
		public void widgetDefaultSelected(SelectionEvent e) {
			widgetSelected(e);
		}
	};

	/**
	 * Scroll wheel event handling.
	 */
	public void handleEvent(Event event) {
		if (event.count > 0) {
			if (topRow > 0) {
				if (!fireRequestRowChangeEvent()) {
					return;
				}
				deselectCurrentRowIfVisible();
				setTopRow(topRow - 1);
				++currentRow;
				if (currentRow == 0) {
					deferredSetFocus(getControl(currentColumn, currentRow), true);
				}
			}
		} else {
			if (topRow < numRowsInCollection - numRowsVisible) {
				if (!fireRequestRowChangeEvent()) {
					return;
				}
				deselectCurrentRowIfVisible();
				setTopRow(topRow + 1);
				--currentRow;
				if (currentRow == getNumRowsVisible()-1) {
					deferredSetFocus(getControl(currentColumn, currentRow), true);
				}
			}
		}
	}

	private void deselectCurrentRowIfVisible() {
		if (currentRow >= 0 && currentRow < numRowsVisible) {
			Control control = getControl(currentColumn, currentRow);
			if (control != null) {
				deselect(control);
			}
		}
	}

	/**
	 * Handle focusLost events on any child control. This is not currently used.
	 * 
	 * @param sender
	 *            The row containing the sending control.
	 * @param e
	 *            The SWT FocusEvent.
	 */
	public void focusLost(TableRow sender, FocusEvent e) {
	}

	/**
	 * Handle focusGained events on any child control.
	 * 
	 * FIXME: Needs to automatically scroll horizontally if the newly-focused
	 * control is fully or partially occluded.
	 * 
	 * @param sender
	 *            The row containing the sending control.
	 * @param e
	 *            The SWT FocusEvent.
	 */
	public void focusGained(TableRow sender, FocusEvent e) {
		boolean rowChanged = false;
		int senderRowNumber = getRowNumber(sender);
		if (senderRowNumber != currentRow) {
			if (needToRequestRC) {
				if (!fireRequestRowChangeEvent()) {
					// Go back if we're not allowed to be here
					deferredSetFocus(getControl(currentColumn, currentRow),
							false);
				}
			} else {
				needToRequestRC = true;
			}
			rowChanged = true;
		}

		currentRow = senderRowNumber;
		currentColumn = sender.getColumnNumber((Control) e.widget);

		if (rowChanged)
			fireRowArriveEvent();
	}

	private PaintListener headerPaintListener = new PaintListener() {
		public void paintControl(PaintEvent e) {
			if (parent.linesVisible) {
				drawGridLines(e, true);
			}
		}
	};

	private PaintListener rowPaintListener = new PaintListener() {
		public void paintControl(PaintEvent e) {
			if (parent.linesVisible) {
				drawGridLines(e, false);
			}
		}
	};

	private void drawGridLines(PaintEvent e, boolean isHeader) {
		Color oldColor = e.gc.getForeground();
		try {
			// Get the colors we need
			Display display = Display.getCurrent();
			Color lineColor = display
					.getSystemColor(SWT.COLOR_WIDGET_DARK_SHADOW);
			Color secondaryColor = display
					.getSystemColor(SWT.COLOR_WIDGET_NORMAL_SHADOW);
			Color hilightColor = display
					.getSystemColor(SWT.COLOR_WIDGET_HIGHLIGHT_SHADOW);
			if (!isHeader) {
				lineColor = display
						.getSystemColor(SWT.COLOR_WIDGET_LIGHT_SHADOW);
			}

			// Get the control
			Control toPaint = (Control) e.widget;
			Point controlSize = toPaint.getSize();

			// Draw the bottom line(s)
			e.gc.setForeground(lineColor);
			e.gc.drawLine(0, controlSize.y - 1, controlSize.x,
					controlSize.y - 1);
			if (isHeader) {
				e.gc.setForeground(secondaryColor);
				e.gc.drawLine(0, controlSize.y - 2, controlSize.x,
						controlSize.y - 2);
				e.gc.setForeground(hilightColor);
				e.gc.drawLine(0, 1, controlSize.x, 1);
			}

			// Now draw lines around the child controls, if there are any
			if (toPaint instanceof Composite) {
				Composite row = (Composite) toPaint;
				Control[] children = row.getChildren();
				for (int i = 0; i < children.length; i++) {
					Rectangle childBounds = children[i].getBounds();

					// Paint the beginning lines
					if (isHeader) {
						e.gc.setForeground(hilightColor);
						e.gc.drawLine(childBounds.x - 2, 1, childBounds.x - 2,
								controlSize.y - 2);
					}

					// Paint the ending lines
					e.gc.setForeground(lineColor);
					int lineLeft = childBounds.x + childBounds.width + 1;
					e.gc.drawLine(lineLeft, 0, lineLeft, controlSize.y);
					if (isHeader) {
						e.gc.setForeground(secondaryColor);
						e.gc.drawLine(lineLeft - 1, 0, lineLeft - 1,
								controlSize.y - 1);
					}
				}
			}
		} finally {
			e.gc.setForeground(oldColor);
		}
	}

	// Event Firing
	// -------------------------------------------------------------------------------

	/**
	 * Fire the row construction event
	 * 
	 * @param newControl
	 *            The new row's SWT control
	 */
	private void fireRowConstructionEvent(Control newControl) {
		for (Iterator rowConstructionListenersIter = parent.rowConstructionListeners
				.iterator(); rowConstructionListenersIter.hasNext();) {
			RowConstructionListener listener = (RowConstructionListener) rowConstructionListenersIter
					.next();
			listener.rowConstructed(newControl);
		}
	}

	/**
	 * Fire the row construction event
	 * 
	 * @param newControl
	 *            The new row's SWT control
	 */
	private void fireHeaderConstructionEvent(Control newControl) {
		for (Iterator rowConstructionListenersIter = parent.rowConstructionListeners
				.iterator(); rowConstructionListenersIter.hasNext();) {
			RowConstructionListener listener = (RowConstructionListener) rowConstructionListenersIter
					.next();
			listener.headerConstructed(newControl);
		}
	}

	/**
	 * Indicate to listeners that the focus is arriving on the specified row
	 */
	private void fireRowArriveEvent() {
		if (rows.size() < 1 || currentRow < 0) {
			return;
		}
		for (Iterator rowChangeListenersIter = parent.rowFocusListeners
				.iterator(); rowChangeListenersIter.hasNext();) {
			IRowFocusListener listener = 
				(IRowFocusListener) rowChangeListenersIter.next();
			// currentRow() can be null if it's scrolled off the top or bottom
			TableRow row = currentRow();
			Control control = row != null ? row.getRowControl() : null;
			listener.arrive(parent, topRow + currentRow, control);
		}
	}

	/**
	 * Request permission from all listeners to leave the current row.
	 * 
	 * @return true if all listeners permit the row change; false otherwise.
	 */
	private boolean fireRequestRowChangeEvent() {
		if (rows.size() < 1 || (currentRow < 1 && topRow != 0)) {
			return true;
		}
		if (currentRow > rows.size() - 1) {
			// (if the other row is already gone)
			return true;
		}
		for (Iterator rowChangeListenersIter = parent.rowFocusListeners
				.iterator(); rowChangeListenersIter.hasNext();) {
			IRowFocusListener listener = (IRowFocusListener) rowChangeListenersIter
					.next();
			// currentRow() can be null if it's scrolled off the top or bottom
			TableRow row = currentRow();
			Control control = row != null ? row.getRowControl() : null;
			if (!listener.requestRowChange(parent, topRow + currentRow,
					control)) {
				return false;
			}
		}
		fireRowDepartEvent();
		return true;
	}

	/**
	 * Indicate to listeners that the focus is about to leave the current row.
	 */
	private void fireRowDepartEvent() {
		if (rows.size() < 1 || (currentRow < 1 && topRow != 0)) {
			return;
		}
		for (Iterator rowChangeListenersIter = parent.rowFocusListeners
				.iterator(); rowChangeListenersIter.hasNext();) {
			IRowFocusListener listener = (IRowFocusListener) rowChangeListenersIter
					.next();
			// currentRow() can be null if it's scrolled off the top or bottom
			TableRow row = currentRow();
			Control control = row != null ? row.getRowControl() : null;
			if (control != null)
				listener.depart(parent, topRow + currentRow, control);
		}
	}

	/**
	 * Request deletion of the current row from the underlying data structure.
	 * 
	 * @return true if the deletion was successful; false otherwise.
	 */
	private boolean fireDeleteEvent() {
		if (parent.deleteHandlers.size() < 1) {
			return false;
		}

		int absoluteRow = topRow + currentRow;
		for (Iterator deleteHandlersIter = parent.deleteHandlers.iterator(); deleteHandlersIter
				.hasNext();) {
			IDeleteHandler handler = (IDeleteHandler) deleteHandlersIter.next();
			if (!handler.canDelete(absoluteRow)) {
				return false;
			}
		}
		for (Iterator deleteHandlersIter = parent.deleteHandlers.iterator(); deleteHandlersIter
				.hasNext();) {
			IDeleteHandler handler = (IDeleteHandler) deleteHandlersIter.next();
			handler.deleteRow(absoluteRow);
		}
		return true;
	}

    private void fireRowDeletedEvent() {
        int absoluteRow = topRow + currentRow;
        for (Iterator deleteHandlersIter = parent.deleteHandlers.iterator(); deleteHandlersIter
                .hasNext();) {
            IDeleteHandler handler = (IDeleteHandler) deleteHandlersIter.next();
            handler.rowDeleted(absoluteRow);
        }
    }

    /**
	 * Request that the model insert a new row into itself.
	 * 
	 * @return The 0-based offset of the new row from the start of the
	 *         collection or -1 if a new row could not be inserted.
	 */
	private int fireInsertEvent() {
		if (parent.insertHandlers.size() < 1) {
			return -1;
		}

		for (Iterator insertHandlersIter = parent.insertHandlers.iterator(); insertHandlersIter
				.hasNext();) {
			IInsertHandler handler = (IInsertHandler) insertHandlersIter.next();
			int resultRow = handler.insert(topRow + currentRow);
			if (resultRow >= 0) {
				return resultRow;
			}
		}

		return -1;
	}
	
	/**
	 * Tell listeners that we just scrolled.
	 * @param scrollEvent TODO
	 */
	private void fireScrollEvent(ScrollEvent scrollEvent) {
		if (parent.scrollListeners.size() < 1) {
			return;
		}
		
		for (Iterator scrollListenersIter = parent.scrollListeners.iterator(); scrollListenersIter.hasNext();) {
			ScrollListener scrollListener = (ScrollListener) scrollListenersIter.next();
			scrollListener.tableScrolled(scrollEvent);
		}
	}

	// Event Handling, utility methods
	// ------------------------------------------------------------

	/**
	 * Set the widget's selection to an empty selection.
	 * 
	 * @param widget
	 *            The widget to deselect
	 */
	private void deselect(Widget widget) {
		if (DuckType.instanceOf(ISelectableRegionControl.class, widget)) {
			ISelectableRegionControl control = (ISelectableRegionControl) DuckType
					.implement(ISelectableRegionControl.class, widget);
			control.setSelection(0, 0);
		}
	}

	/**
	 * Try to go to the next row in the collection.
	 */
	private void handleNextRowNavigation() {
		if (currentRow < numRowsVisible - 1) {
			if (!fireRequestRowChangeEvent()) {
				return;
			}
			needToRequestRC = false;

			deselect(getControl(currentColumn, currentRow));

			deferredSetFocus(getControl(0, currentRow + 1), false);
		} else {
			if (topRow + numRowsVisible >= numRowsInCollection) {
				// We're at the end; don't go anywhere
				return;
			}
			// We have to scroll forwards
			if (!fireRequestRowChangeEvent()) {
				return;
			}
			needToRequestRC = false;

			deselect(getControl(currentColumn, currentRow));

			setTopRow(topRow + 1);
			deferredSetFocus(getControl(0, currentRow), true);
		}
	}

	/**
	 * Try to go to the previous row in the collection.
	 * 
	 * @param row
	 *            The current table row.
	 */
	private void handlePreviousRowNavigation(TableRow row) {
		if (currentRow == 0) {
			if (topRow == 0) {
				// We're at the beginning of the table; don't go anywhere
				return;
			}
			// We have to scroll backwards
			if (!fireRequestRowChangeEvent()) {
				return;
			}
			needToRequestRC = false;

			deselect(getControl(currentColumn, currentRow));

			setTopRow(topRow - 1);
			deferredSetFocus(getControl(row.getNumColumns() - 1, 0), true);
		} else {
			if (!fireRequestRowChangeEvent()) {
				return;
			}
			needToRequestRC = false;

			deselect(getControl(currentColumn, currentRow));

			deferredSetFocus(
					getControl(row.getNumColumns() - 1, currentRow - 1), false);
		}
	}

	/**
	 * Gets the current TableRow.
	 * 
	 * @return the current TableRow
	 */
	private TableRow currentRow() {
		if (currentRow > rows.size() - 1) {
			return null;
		}
		return (TableRow) rows.get(currentRow);
	}

	/**
	 * Returns the SWT control corresponding to the current row.
	 * 
	 * @return the current row control.
	 */
	public Control getCurrentRowControl() {
		TableRow currentRow = currentRow();
		if (currentRow == null) {
			return null;
		}
		return currentRow().getRowControl();
	}
	
	/**
	 * Method getRowControls. Returns an array of SWT controls where each
	 * control represents a row control in the CompositeTable's current scrolled
	 * position. If CompositeTable is resized, scrolled, such that the rows that
	 * the CompositeTable control is displaying change in any way, the array
	 * that is returned by this method will become out of date and need to be
	 * retrieved again.
	 * 
	 * @return Control[] An array of SWT Control objects, each representing an
	 *         SWT row object.
	 */
	public Control[] getRowControls() {
		Control[] rowControls = new Control[rows.size()];
		for (int i = 0; i < rowControls.length; i++) {
			rowControls[i] = getRowByNumber(i).getRowControl();
		}
		return rowControls;
	}
	
	private Menu menu = null;
	
	/* (non-Javadoc)
	 * @see org.eclipse.swt.widgets.Control#setMenu(org.eclipse.swt.widgets.Menu)
	 */
	public void setMenu(final Menu menu) {
		this.menu = menu;
		setMenuOnCollection(rows, menu);
		setMenuOnCollection(spareRows, menu);
	}

	private void setMenuOnCollection(LinkedList collection, Menu menu) {
		for (Iterator rowsIter = collection.iterator(); rowsIter.hasNext();) {
			TableRow row = (TableRow) rowsIter.next();
			row.getRowControl().setMenu(menu);
		}
	}
	
	

	/**
	 * Method getControlRow.  Given a row control, returns its row number
	 * relative to the topRow.
	 * 
	 * @param rowControl The row object to find
	 * @return The row number of the rowControl relative to the topRow (0-based)
	 * @throws IllegalArgumentException if rowControl is not currently visible
	 */
	public int getControlRow(Control rowControl) {
		for (int row = 0; row < rows.size(); row++) {
			if (getRowByNumber(row).getRowControl() == rowControl) {
				return row;
			}
		}
		throw new IllegalArgumentException("getControlRow passed a control that is not visible inside CompositeTable");
	}
	
	/**
	 * Method getControlRowObject.  Given a row control, returns its row number
	 * relative to the topRow.
	 * 
	 * @param rowControl The row object to find
	 * @return The row object managing the rowControl
	 * @throws IllegalArgumentException if rowControl is not currently visible
	 */
	public TableRow getControlRowObject(Control rowControl) {
		for (Iterator rowsIter = rows.iterator(); rowsIter.hasNext();) {
			TableRow row = (TableRow) rowsIter.next();
			if (row.getRowControl() == rowControl) {
				return row;
			}
		}
		throw new IllegalArgumentException("getControlRowObject passed a control that is not visible inside CompositeTable");
	}

	/**
	 * Returns the TableRow by the specified 0-based offset from the top visible
	 * row.
	 * 
	 * @param rowNumber
	 *            0-based offset of the requested fow starting from the top
	 *            visible row.
	 * @return The corresponding TableRow or null if there is none.
	 */
	private TableRow getRowByNumber(int rowNumber) {
		if (rowNumber > rows.size() - 1 || rowNumber < 0) {
			return null;
		}
		return (TableRow) rows.get(rowNumber);
	}

	/**
	 * Return the SWT control at (column, row), where row is a 0-based number
	 * starting from the top visible row.
	 * 
	 * @param column
	 *            the 0-based column.
	 * @param row
	 *            the 0-based row starting from the top visible row.
	 * @return the SWT control at (column, row)
	 */
	private Control getControl(int column, int row) {
		TableRow rowObject = getRowByNumber(row);
		if (rowObject == null) {
			throw new IndexOutOfBoundsException("Request for a nonexistent row"); //$NON-NLS-1$
		}
		Control result = rowObject.getColumnControl(column);
		return result;
	}

	/**
	 * Return the 0-based row number corresponding to a particular TableRow
	 * object.
	 * 
	 * @param row
	 *            The TableRow to translate to row coordinates.
	 * @return the 0-based row number or -1 if the specified TableRow is not
	 *         visible.
	 */
	private int getRowNumber(TableRow row) {
		int rowNumber = 0;
		for (Iterator rowIter = rows.iterator(); rowIter.hasNext();) {
			TableRow candidate = (TableRow) rowIter.next();
			if (candidate == row) {
				return rowNumber;
			}
			++rowNumber;
		}
		return -1;
	}

	/**
	 * Set the focus to the specified (column, row). If rowChange is true, fire
	 * a row change event, otherwise be silent.
	 * 
	 * @param column
	 *            The 0-based column to focus
	 * @param row
	 *            The 0-based row to focus
	 * @param rowChange
	 *            true if a row change event should be fired; false otherwise.
	 */
	private void internalSetSelection(int column, int row, boolean rowChange) {
		Control toFocus = getControl(column, row);
		if (toFocus == null) {
			return;
		}
		if (toFocus.isFocusControl()) {
			toFocus.notifyListeners(SWT.FocusIn, new Event());
		} else {
			deferredSetFocus(toFocus, rowChange);
		}
	}

	/**
	 * Set the focus to the specified control after allowing all pending events
	 * to complete first. If rowChange is true, fire a row arrive event after
	 * the focus has been set.
	 * 
	 * @param toFocus
	 *            The SWT Control to focus
	 * @param rowChange
	 *            true if the rowArrive event should be fired; false otherwise.
	 */
	private void deferredSetFocus(final Control toFocus, final boolean rowChange) {
		if (toFocus == null) {
			return;
		}
		Display.getCurrent().asyncExec(new Runnable() {
			public void run() {
				if (toFocus.isDisposed()) return;
				toFocus.setFocus();
				if (rowChange) {
					fireRowArriveEvent();
				}
			}
		});
	}

    public void doFocusInitialRow() {
        if (topRow <= 0) {
            return;
        }

        if (!fireRequestRowChangeEvent()) {
            return;
        }
        needToRequestRC = false;

        Widget widget = getDisplay().getFocusControl();
        deselect(widget);  // Used to be e.widget

        // If the focus is already in the top visible row, we will need
        // to explicitly
        // fire an arrive event.
        boolean needToArrive = true;
        if (currentRow > 0) {
            needToArrive = false;
        }

        setTopRow(0);

        if (needToArrive) {
            internalSetSelection(currentColumn, 0, true);
        } else {
            internalSetSelection(currentColumn, 0, false);
        }
    }

    public void doFocusLastRow() {
        if (topRow + numRowsVisible < numRowsInCollection) {
            if (!fireRequestRowChangeEvent()) {
                return;
            }
            needToRequestRC = false;

            Widget widget = getDisplay().getFocusControl();
            deselect(widget);  // Used to be e.widget

            // If the focus is already in the last visible row, we will
            // need to explicitly
            // fire an arrive event.
            boolean needToArrive = true;
            if (currentRow < numRowsVisible - 1) {
                needToArrive = false;
            }

            setTopRow(numRowsInCollection - numRowsVisible);

            if (needToArrive) {
                internalSetSelection(currentColumn, numRowsVisible - 1,
                        true);
            } else {
                internalSetSelection(currentColumn, numRowsVisible - 1,
                        false);
            }
        }
    }

    public void doPageUp() {
        if (topRow > 0) {
            if (!fireRequestRowChangeEvent()) {
                return;
            }
            needToRequestRC = false;

            int newTopRow = topRow - numRowsInDisplay;
            if (newTopRow < 0) {
                newTopRow = 0;
            }
            setTopRow(newTopRow);
            internalSetSelection(currentColumn, currentRow, true);
        }
    }

    public void doPageDown() {
        if (topRow + numRowsVisible < numRowsInCollection) {
            if (!fireRequestRowChangeEvent()) {
                return;
            }
            needToRequestRC = false;

            int newTopRow = topRow + numRowsVisible;
            if (newTopRow >= numRowsInCollection - 1) {
                newTopRow = numRowsInCollection - 1;
            }
            setTopRow(newTopRow);
            internalSetSelection(currentColumn, currentRow, true);
        }
    }

    public void doRowUp() {
        if (maxRowsVisible <= 1)
            return;

        if (currentRow > 0) {
            if (!fireRequestRowChangeEvent()) {
                return;
            }
            needToRequestRC = false;

            Widget widget = getDisplay().getFocusControl();
            deselect(widget);  // Used to be e.widget

            internalSetSelection(currentColumn, currentRow - 1, false);
            return;
        }
        if (topRow > 0) {
            if (!fireRequestRowChangeEvent()) {
                return;
            }
            needToRequestRC = false;

            Widget widget = getDisplay().getFocusControl();
            deselect(widget);  // Used to be e.widget

            setTopRow(topRow - 1);
            internalSetSelection(currentColumn, currentRow, true);
            return;
        }
    }

    public void doRowDown() {
        if (maxRowsVisible <= 1)
            return;

        if (currentRow < numRowsVisible - 1) {
            if (!fireRequestRowChangeEvent()) {
                return;
            }
            needToRequestRC = false;

            Widget widget = getDisplay().getFocusControl();
            deselect(widget);  // Used to be e.widget

            internalSetSelection(currentColumn, currentRow + 1, false);
            return;
        }
        if (topRow + numRowsVisible < numRowsInCollection) {
            if (!fireRequestRowChangeEvent()) {
                return;
            }
            needToRequestRC = false;

            Widget widget = getDisplay().getFocusControl();
            deselect(widget);  // Used to be e.widget

            setTopRow(topRow + 1);
            internalSetSelection(currentColumn, currentRow, true);
            return;
        }
    }

    public boolean doInsertRow() {
        // If no insertHandler has been registered, bail out
        if (parent.insertHandlers.size() < 1) {
            return false;
        }

        // Make sure we can leave the current row
        if (!fireRequestRowChangeEvent()) {
            return false;
        }
        needToRequestRC = false;

        // Insert the new object
        int newRowPosition = fireInsertEvent();
        if (newRowPosition < 0) {
            // This should never happen, but...
            throw new IllegalArgumentException("Insert < row 0???");
        }

        disposeEmptyTablePlaceholder();

        // If the current widget has a selection, deselect it
        Widget widget = getDisplay().getFocusControl();
        deselect(widget);  // Used to be e.widget

        // If the new row is in the visible space, refresh it
        if (topRow <= newRowPosition
                && numRowsVisible > newRowPosition - topRow) {
            insertRowAt(newRowPosition - topRow);
            ++numRowsInCollection;
            updateVisibleRows();
            int newRowNumber = newRowPosition - topRow;
            if (newRowNumber != currentRow) {
                internalSetSelection(currentColumn, newRowNumber, false);
            } else {
                internalSetSelection(currentColumn, newRowNumber, true);
            }
            return true;
        }

        // else...

        ++numRowsInCollection;

        // If the new row is above us, scroll up to it
        if (newRowPosition < topRow + currentRow) {
            setTopRow(newRowPosition);
            Display.getDefault().asyncExec(new Runnable() {
                public void run() {
                    updateVisibleRows();
                    if (currentRow != 0) {
                        internalSetSelection(currentColumn, 0, false);
                    } else {
                        internalSetSelection(currentColumn, 0, true);
                    }
                }
            });
        } else {
            // If we're appending
            if (numRowsInDisplay > numRowsVisible) {
                updateVisibleRows();
                int newRowNumber = newRowPosition - topRow;
                if (newRowNumber != currentRow) {
                    internalSetSelection(currentColumn, newRowNumber,
                            false);
                } else {
                    internalSetSelection(currentColumn, newRowNumber,
                            true);
                }
            } else {
                // It's somewhere in the middle below us; scroll down to
                // it
                setTopRow(newRowPosition - numRowsVisible + 1);
                int newRowNumber = numRowsVisible - 1;
                if (newRowNumber != currentRow) {
                    internalSetSelection(currentColumn, newRowNumber,
                            false);
                } else {
                    internalSetSelection(currentColumn, newRowNumber,
                            true);
                }
            }
        }
        return false;
    }
    
    public boolean doDeleteRow() {
        if (fireDeleteEvent()) {
            // We know the object is gone if we made it here, so now
            // refresh the display...
            --numRowsInCollection;

            // If we deleted the last row in the list
            if (currentRow >= numRowsVisible - 1) {
                // If that wasn't the last row in the collection, move
                // the focus
                if (numRowsInCollection > 0) {

                    // If we're only displaying one row, scroll first
                    if (currentRow < 1) {
                        needToRequestRC = false;
                        deleteRowAt(currentRow);
                        setTopRow(topRow - 1);
                        internalSetSelection(currentColumn, currentRow,
                                true);
                    } else {
                        needToRequestRC = false;
                        internalSetSelection(currentColumn,
                                currentRow - 1, false);
                        Display.getCurrent().asyncExec(new Runnable() {
                            public void run() {
                                deleteRowAt(currentRow + 1);
                                updateVisibleRows();
                            }
                        });
                    }
                } else {
                    // Otherwise, show the placeholder object and give
                    // it focus
                    deleteRowAt(currentRow);
                    --numRowsVisible;
                    createEmptyTablePlaceholer();
                    emptyTablePlaceholder.setFocus();
                }
            } else {
                // else, keep the focus where it was
                deleteRowAt(currentRow);
                updateVisibleRows();
                internalSetSelection(currentColumn, currentRow, true);
            }
            fireRowDeletedEvent();
        }
        return false;
    }


}